package utils.math_utils;

import java.util.Random;

public class RandStream {

	public enum Rand {
		GAUSS, UNIFORM, LOGNORMAL, POISSON, POISSON_INTERVALS;
	}

	private static long randSeed = 0;
	
	private double mean;
	public double getMean() {
		return mean;
	}

	public double getSigma() {
		return sigma;
	}

	public boolean isClipping() {
		return clipping;
	}

	private double sigma;
	private double max;
	public void setMax(double max) {
		this.max = max;
	}

	public void setMin(double min) {
		this.min = min;
	}

	private double min;
	private  boolean clipping;
	private static Random rand = new Random();
	private Rand rType;
	
	public RandStream() {
		rand = new Random();
	}
	
	public RandStream(Rand rType, double mean, double sigma, double min,
			double max, boolean clipping) {
		this.rType = rType;
		this.min = min;
		this.max = max;
		this.mean = mean;
		this.sigma = sigma;
		this.clipping = clipping;
		if(randSeed != 0) {
			rand = new Random(randSeed);
		} else {
			rand = new Random();
		}
	}
	
	public RandStream(double max, double min) {
		this(Rand.UNIFORM, 0, 0, max, min, true);
	}
		
	public RandStream(double mean, boolean clipping) {
		this(Rand.POISSON, mean, 0, mean*10, 1, clipping);
	}
	
	public RandStream(double mean) {
		max = 0;
		min = 0;
		rType = Rand.POISSON_INTERVALS;
		setMean(mean);
	}
	
	
	public double next(){
		if(rType == Rand.UNIFORM) {
			return (max-min) * rand.nextDouble() + min;
		} else if (rType == Rand.GAUSS){
			double val = rand.nextGaussian();
			val = sigma * val + mean;
			if(clipping) {
				if (val  > max) {		
					val = nextUniform(mean, max);
				} else if (val < min) {
					val = nextUniform(min, mean);
				} else {
					return val;
				}			
			}
			return val;
		} else if (rType == Rand.LOGNORMAL) {
			double val = rand.nextGaussian();
			val = Math.log(sigma) * val + Math.log(mean);
			val = Math.exp(val);
			if(clipping) {
				if (val  > max) {		
					val = nextUniform(mean, max);
				} else if (val < min) {
					val = nextUniform(min, mean);
				} else {
					return val;
				}
			}
			return val;
		} else {
			throw new IllegalStateException("RandStream.next() is only" +
					" appropriate for a stream of type Uniform or Gaussian.");
		}
	}

	public static double nextUniform(double min, double max) {
		return (max-min) * rand.nextDouble() + min;
	}
	
	public static double nextGaussian(double mean, double sigma, double min,
			double max, boolean clipping) {
		double val = rand.nextGaussian();
		val = sigma * val + mean;
		if(clipping){
			if (val  > max) {		
				val = nextUniform(min, max);
			} else if (val < min) {
				val = nextUniform(min, max);
			} else {
				return val;
			}
		}
		return val;
	}
	
	/**
	 * Samples from a poisson distribution with a mean number of events 
	 * per time interval equal to RandStream's mean variable. Samples are
	 * made using Knuth's algorithm, all credit to Donald Knuth.
	 * @return the number of events per interval, poisson
	 */
	public int nextPoisson() {
		double l = Math.exp(-mean);
		double p = 1;
		int k = 0;
		do {
			k++;
			p *= Math.random();
		} while (p > l);
		return k;
	}
	
	/**
	 * Returns the time between poisson distributed events, using RandStream's
	 * mean variable.
	 * @return time until the next signal of a homogenous poisson process
	 */
	public double nextPoissonInterval() {
		double r = Math.random();
		return -Math.log(r + 0.0001) / mean;
	}
	
	/**
	 * 
	 * @param frequency
	 * @return
	 */
	public static double nextPoissonInterval(double frequency) {
		double r;
		do{
			r = Math.random();
		} while (r == 0);
		return -Math.log(r) / frequency;
	}
	
	public void setMeanSD(double mean, double sigma) {
		setMean(mean);
		setSigma(sigma);
	}
	
	public void setMean(double mean) {
		this.mean = mean;
	}
	
	public void setSigma(double sigma) {
		this.sigma = sigma;
	}
	
	public void setClipping(boolean clipping) {
		this.clipping = clipping;
	}

	public static double getRandSeed() {
		return randSeed;
	}

	public static void setRandSeed(long randSeed) {
		RandStream.randSeed = randSeed;
	}

	public Rand getrType() {
		return rType;
	}

	public double getMax() {
		return max;
	}

	public double getMin() {
		return min;
	}
	public void setrType(Rand rType) {
		this.rType = rType;
	}

	public static Random getRand() {
		return rand;
	}

	public static void setRand(Random rand) {
		RandStream.rand = rand;
	}
}
